﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Microsoft.Xna.Framework;

namespace jvox
{
    public class OctreeNode
    {
        public Vector3 min; //The corner point with lowest X,Y,Z coords
        public Vector3 max; //The corner point with the highest X,Y,Z coords

        public static uint numVoxels = 0;
        private static Random rand = new Random();
        //Octants: xyz
        // 0 = + + +
        // 1 = - + +
        // 2 = + - +
        // 3 = - - +
        // 4 = + + -
        // 5 = - + -
        // 6 = + - -
        // 7 = - - -

        public OctreeNode[] octants;
        public Color? color;

        public OctreeNode()
        {
            octants = new OctreeNode[8];
            numVoxels++;
        }

        //Test if ray intersects with this Node
        public bool RayIntersects(ref Ray ray)
        {
            BoundingBox b = new BoundingBox(min, max);
            return b.Intersects(ray) != null;
        }

        public void generateTestChildNodes(int level)
        {
            const int MAXLEVEL = 10;
            
            if (level >= MAXLEVEL)
            {
                this.color = new Color(rand.Next(0, 254), rand.Next(0, 254), rand.Next(0, 254));
            }
            else
            {
                bool made = false;
                for (int i = 0; i < 8; i++)
                {
                    if (rand.NextDouble() > 0.7)
                    {
                        this.octants[i] = new OctreeNode();
                        BoundingBox bb = this.GetBoundingBoxOfOctant(i);
                        this.octants[i].min = bb.Min;
                        this.octants[i].max = bb.Max;
                        if (rand.NextDouble() < 0.8)
                        {
                            made = true;
                            this.octants[i].generateTestChildNodes(level + 1);
                        }
                        else
                        {
                            made = true;
                            this.octants[i].color = new Color(rand.Next(0, 254), rand.Next(0, 254), rand.Next(0, 254));
                        }
                    }
                }
                if (!made)
                    this.color = new Color(rand.Next(0, 254), rand.Next(0, 254), rand.Next(0, 254));
            }
        }

        private struct DistanceOctant
        {
            public float distance;
            public int octant;

            public DistanceOctant(float distance, int octant)
            {
                this.distance = distance;
                this.octant = octant;
            }
        }

        public Vector3 GetCenterOfOctant(int octant)
        {
            Vector3 lengths = new Vector3(max.X - min.X, max.Y - min.Y, max.Z - min.Z);
            switch (octant)
            {
                case 0:
                    //+++
                    return new Vector3(max.X - 0.25f * lengths.X, max.Y - 0.25f * lengths.Y, max.Z - 0.25f * lengths.Z);
                case 1:
                    //-++
                    return new Vector3(min.X + 0.25f * lengths.X, max.Y - 0.25f * lengths.Y, max.Z - 0.25f * lengths.Z);
                case 2:
                    //+-+
                    return new Vector3(max.X - 0.25f * lengths.X, min.Y + 0.25f * lengths.Y, max.Z - 0.25f * lengths.Z);
                case 3:
                    //--+
                    return new Vector3(min.X + 0.25f * lengths.X, min.Y + 0.25f * lengths.Y, max.Z - 0.25f * lengths.Z);
                case 4:
                    //++-
                    return new Vector3(max.X - 0.25f * lengths.X, max.Y - 0.25f * lengths.Y, min.Z + 0.25f * lengths.Z);
                case 5:
                    //-+-
                    return new Vector3(min.X + 0.25f * lengths.X, max.Y - 0.25f * lengths.Y, min.Z + 0.25f * lengths.Z);
                case 6:
                    //+--
                    return new Vector3(max.X - 0.25f * lengths.X, min.Y + 0.25f * lengths.Y, min.Z + 0.25f * lengths.Z);
                case 7:
                    //---
                    return new Vector3(min.X + 0.25f * lengths.X, min.Y + 0.25f * lengths.Y, min.Z + 0.25f * lengths.Z);
                default:
                    throw new IndexOutOfRangeException("Octant index out of range, must be 0-7");
            }
        }

        public BoundingBox GetBoundingBoxOfOctant(int octant)
        {
            BoundingBox result = new BoundingBox();
            Vector3 center = (min + max) / 2f;
            switch (octant)
            {
                case 0:
                    //+++
                    result.Min = center;
                    result.Max = max;
                    break;
                case 1:
                    //-++
                    result.Min = new Vector3(min.X, center.Y, center.Z);
                    result.Max = new Vector3(center.X, max.Y, max.Z);
                    break;
                case 2:
                    //+-+
                    result.Min = new Vector3(center.X, min.Y, center.Z);
                    result.Max = new Vector3(max.X, center.Y, max.Z);
                    break;
                case 3:
                    //--+
                    result.Min = new Vector3(min.X, min.Y, center.Z);
                    result.Max = new Vector3(center.X, center.Y, max.Z);
                    break;
                case 4:
                    //++-
                    result.Min = new Vector3(center.X, center.Y, min.Z);
                    result.Max = new Vector3(max.X, max.Y, center.Z);
                    break;
                case 5:
                    //-+-
                    result.Min = new Vector3(min.X, center.Y, min.Z);
                    result.Max = new Vector3(center.X, max.Y, center.Z);
                    break;
                case 6:
                    //+--
                    result.Min = new Vector3(center.X, min.Y, min.Z);
                    result.Max = new Vector3(max.X, center.Y, center.Z);
                    break;
                case 7:
                    //---
                    result.Min = new Vector3(min.X, min.Y, min.Z);
                    result.Max = new Vector3(center.X, center.Y, center.Z);
                    break;
                default:
                    throw new IndexOutOfRangeException("Octant index out of range, must be 0-7");
            }
            return result;
        }

        public Color? GetIntersectingVoxel(ref Ray ray)
        {
            //If this intersects
                //If this has a color, return it;
                //Sort octants based on proximity to the ray's position
                //for each octant
                //if it intersects && octant is occupied, test GetIntersectingVoxel of that octant
                    //If GetIntersectingVoxel returns a color return that
                    //Else try next octant
            if (new BoundingBox(this.min, this.max).Intersects(ray) == null)
            {
                return null;
            }
            if (this.color != null)
            {
                return color;
            }
            DistanceOctant[] dos = new DistanceOctant[8];
            for (int i = 0; i < octants.Length; i++)
            {
                if (octants[i] != null)
                {
                    dos[i].octant = i;
                    dos[i].distance = Vector3.Distance(GetCenterOfOctant(i), ray.Position);
                }
                else
                {
                    dos[i].octant = -1;
                }
            }
            Array.Sort<DistanceOctant>(dos, (d1,d2) => d1.distance.CompareTo(d2.distance));
            Color? result = null;
            for (int i = 0; i < dos.Length && result == null; i++)
            {
                if (dos[i].octant >= 0) {
                    result = octants[dos[i].octant].GetIntersectingVoxel(ref ray);
                }
            }
            return result;
        }
    }
}
